#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <time.h>
#include <dirent.h>
#include <sys/stat.h>
#include "zmalloc.h"
#include "ae.h"
#include "anet.h"
#include "sds.h"

#define MAX_REQUEST_HEAD_SIZE 8000

typedef struct ServerConfig {
    aeEventLoop *evtLoop;
    char *host;
    int port;
    sds docroot;
    int verbosity;
} ServerConfig;

typedef struct HttpData {
    sds buf;
    sds reqmethod;
    sds reqpath;
    int keepalive;

    sds header;
    int fd;
    int headerLen;
    int headerSent;
    int bodyLen;
    int bodySent;
} HttpData;

static void oom(const char *msg)
{
    fprintf(stderr, "%s: Out of Memory\n", msg);
    abort();
}

static char *progName;
static char anetError[ANET_ERR_LEN];
static ServerConfig server;

static HttpData *HttpData_create()
{
    HttpData *hd = zmalloc(sizeof(*hd));
    if (hd) {
        memset(hd, 0, sizeof(*hd));
        hd->buf = sdsempty();
        if (hd->buf == NULL) {
            zfree(hd);
            return NULL;
        }
        hd->fd = -1;
    }
    return hd;
}

static HttpData_reset(HttpData *hd)
{
    hd->buf = sdscpy(hd->buf, "");
    if (hd->reqmethod) {
        sdsfree(hd->reqmethod);
        hd->reqmethod = NULL;
    }
    if (hd->reqpath) {
        sdsfree(hd->reqpath);
        hd->reqpath = NULL;
    }
    if (hd->header) {
        sdsfree(hd->header);
        hd->header = NULL;
    }
    hd->keepalive = 0;
    hd->fd = -1;
    hd->headerLen = 0;
    hd->headerSent = 0;
    hd->bodyLen = 0;
    hd->bodySent = 0;
}

static void *HttpData_free(HttpData *hd)
{
    if (hd) {
        if (hd->buf)
            sdsfree(hd->buf);
        if (hd->reqmethod)
            sdsfree(hd->reqmethod);
        if (hd->reqpath)
            sdsfree(hd->reqpath);
        if (hd->header)
            sdsfree(hd->header);
        zfree(hd);
    }
}

static void sendResponse(aeEventLoop *el, int fd, void *privdata, int mask)
{
    HttpData *hd = privdata;
    int error = 0;

send_header:
    if (hd->headerSent < hd->headerLen) {
        int nwritten;
        nwritten = write(fd, hd->header + hd->headerSent, hd->headerLen - hd->headerSent);
        if (nwritten <= 0) {
            if (errno != EAGAIN) {
                fprintf(stderr, "Writing to client: %s\n", strerror(errno));
                error = 1;
                goto done;
            }
        } else {
            hd->headerSent += nwritten;
            if (hd->headerSent < hd->headerLen)
                return;
            else
                goto send_body;
        }
    }

send_body:
    if (hd->fd > 0 && hd->bodySent < hd->bodyLen) {
#if defined(__APPLE__)
        /* TODO: should really use sf_hdtr to send header and body at the same time */
        unsigned sent = hd->bodyLen - hd->bodySent;    /* Max integer */
        int r = sendfile(hd->fd, fd, 0, &sent, NULL, 0);
        if (r == -1) {
            if (errno == EAGAIN || errno == EINTR) {
                hd->bodySent += sent;
            } else {
                fprintf(stderr, "Writing to client: %s\n", strerror(errno));
                error = 1;
                goto done;
            }
        } else {
            if (sent == 0) {        /* end of file reached */
                error = 1;
                goto done;
            } else
                hd->bodySent += sent;
        }
#elif defined(__linux__)
        /* TODO: use TCP_CORK */
        int sent = sendfile(fd, hd->fd, NULL, hd->bodyLen - hd->bodySent);
        if (sent == -1) {
            if (errno != EAGAIN) {
                fprintf(stderr, "Writing to client: %s\n", strerror(errno));
                error = 1;
                goto done;
            }
        } else if (sent == 0) {     /* end of file reached */
            error = 1;
            goto done;
        } else {
            hd->bodySent += sent;
        }
#else
        /* TODO: should really fallback to plain read/write */
        #error "Sorry, I don't know sendfile but MacOSX and Linux"
#endif
        if (hd->bodySent < hd->bodyLen)
            return;
        else
            goto done;
    }
done:
    aeDeleteFileEvent(el, fd, AE_WRITABLE);
    if (hd->fd > 0)
        close(hd->fd);
    if (error || !hd->keepalive) {
        aeDeleteFileEvent(el, fd, AE_READABLE);
        HttpData_free(hd);
        close(fd);
    } else {
        HttpData_reset(hd);     /* prepare for next request */
    }
}

static void formatDate(char *buf, int n)
{
    time_t t = time(NULL);
    struct tm *gt = gmtime(&t);
    strftime(buf, n, "%a, %d %b %Y %T GMT", gt);
}

static void errorResponse(HttpData *hd, char *status, char *body)
{
    sds header = sdsempty();
    char date[64];
    formatDate(date, sizeof(date));

    header = sdscatprintf(header, "HTTP/1.1 %s\r\n", status);
    header = sdscatprintf(header, "Date: %s\r\n", date);
    header = sdscat(header, "Server: SimpleHTTPD\r\n");
    header = sdscatprintf(header, "Content-length: %lld\r\n", (long long)strlen(body));
    header = sdscat(header, "Content-type: text/plain\r\n");
    header = sdscat(header, "Connection: Close\r\n");
    header = sdscat(header, "\r\n");
    header = sdscat(header, body);
    hd->header = header;
    hd->headerLen = sdslen(header);
}

static void sendErrorResponse(HttpData *hd, int fd, char *status, char *body)
{
    errorResponse(hd, status, body);
    aeCreateFileEvent(server.evtLoop, fd, AE_WRITABLE, sendResponse, hd);
}

static struct MimeEntry { char *name, *type; } mimetab[] = {
        { "html", "text/html" },
        { "css",  "text/css" },
        { "pdf",  "application/pdf" },
        { "gif",  "image/gif" },
        { "png",  "image/png" },
        { "jpeg", "image/jpeg" },
        { "jpg",  "image/jpeg" },
        { "txt"  ,  "text/plain" },
        { "c"  ,  "text/plain" },    /* just for test */
        /* ... more ... */
        { NULL, NULL },
};

static char *mimetype(char *file)
{
    char *dot = strrchr(file, '.');
    if (dot && dot[1]) {
        char *suffix = dot + 1;
        struct MimeEntry *entry;

        for (entry = mimetab; entry->name != NULL; entry++) {
            if (strcasecmp(suffix, entry->name) == 0) {
                return entry->type;
            }
        }
    }
    return "application/octet-stream";
}

static void dirListing(HttpData *hd, sds path)
{
    sds header, body;
    DIR *dir = opendir(path);
    char date[64];

    if (dir == NULL) {
        errorResponse(hd, "404 Not Found", "No such file or directory");
        return;
    } else {
        struct dirent *entry;
        body = sdsempty();
        body = sdscatprintf(body, "<h1>Index of %s</h1><table>", hd->reqpath);
        body = sdscat(body, "<tr><th>Name</th></tr>");
        while ((entry = readdir(dir)) != NULL) {
            char *t = hd->reqpath;
            for (; t[1] == '/'; t++);
            body = sdscatprintf(body, "<tr><td><a href=\"%s/%s\">%s</a></td></tr>", t[1]?hd->reqpath:"", entry->d_name, entry->d_name);
        }
        closedir(dir);
        body = sdscat(body, "</table>");
    }

    header = sdsempty();
    header = sdscat(header, "HTTP/1.1 200 OK\r\n");
    formatDate(date, sizeof(date));
    header = sdscatprintf(header, "Date: %s\r\n", date);
    header = sdscat(header, "Server: SimpleHTTPD\r\n");
    header = sdscatprintf(header, "Content-length: %lld\r\n", (long long)sdslen(body));
    header = sdscat(header, "Content-type: text/html\r\n");
    header = sdscatprintf(header, "Connection: %s\r\n", hd->keepalive ? "Keep-alive" : "Close");
    header = sdscat(header, "\r\n");
    header = sdscatlen(header, body, sdslen(body));
    sdsfree(body);
    hd->header = header;
    hd->headerLen = sdslen(header);
}

static void processRequest(HttpData *hd, int fd)
{
    sds reqbuf = hd->buf;
    int i;
    char *s, *t, *eol;

    eol = strstr(reqbuf, "\r\n");
    s = reqbuf;
    for (t = reqbuf; !isspace(*t) && t < eol; t++);
    hd->reqmethod = sdsnewlen(reqbuf, t-reqbuf);
    for (; isspace(*t) && t < eol; t++);    /* skip spaces */
    s = t;
    for (; !isspace(*t) && t < eol; t++);
    hd->reqpath = sdsnewlen(s, t-s);
    for (; isspace(*t) && t < eol; t++);    /* skip spaces */
    if (strncasecmp(t, "HTTP/1.0", 8) == 0) {
        hd->keepalive = 0;
    } else if (strncasecmp(t, "HTTP/1.1", 8) == 0) {
        hd->keepalive = 1;
    } else {
        errorResponse(hd, "400 Invalid Request", "Invalid Request");
        goto do_response;
    }
    /* Only support GET now */
    if (strncasecmp(hd->reqmethod, "GET", 3) != 0) {
        errorResponse(hd, "400 Invalid Request", "Invalid Method");
        goto do_response;
    }

    /* Find "Connection: xxx" header, set keepalive. */
    s = eol+2;
    eol = strstr(s, "\r\n");
    while (eol) {
        if (strncasecmp(s, "Connection:", 11/* length of "Connection:" */) == 0) {
            t = s + 11;
            for (; isspace(*t) && t < eol; t++);
            if (strncasecmp(t, "Keep-alive", 10) == 0) {
                hd->keepalive = 1;
            } else {
                hd->keepalive = 0;
            }
        }
        s = eol+2;
        eol = strstr(s, "\r\n");
    }
    if (server.verbosity >= 2) {
        fprintf(stderr, "method: %s, path: %s, keepalive: %d\n", hd->reqmethod, hd->reqpath, hd->keepalive);
    }

    {
    sds header, path;
    struct stat st;
    char date[64];

    /* FIXME: should really check path so that it didn't access outside of docroot. */
    /* TODO: directory listing */
    path = sdsdup(server.docroot);
    path = sdscat(path, "/");
    path = sdscatlen(path, hd->reqpath, sdslen(hd->reqpath));
    hd->fd = open(path, O_RDONLY);

    if (hd->fd == -1 || fstat(hd->fd, &st) == -1) {
        errorResponse(hd, "404 Not Found", "No such file or directory");
        goto do_response;
    }
    if (!S_ISREG(st.st_mode) && !S_ISDIR(st.st_mode)) {
        errorResponse(hd, "404 Not Found", "No such file or directory");
        goto do_response;
    }
    if (S_ISDIR(st.st_mode)) {
        close(hd->fd);
        hd->fd = -1;
        dirListing(hd, path);
        goto do_response;
    }
    sdsfree(path);

    header = sdsempty();
    header = sdscat(header, "HTTP/1.1 200 OK\r\n");
    formatDate(date, sizeof(date));
    header = sdscatprintf(header, "Date: %s\r\n", date);
    header = sdscat(header, "Server: SimpleHTTPD\r\n");
    header = sdscatprintf(header, "Content-length: %lld\r\n", (long long)st.st_size);
    header = sdscatprintf(header, "Content-type: %s\r\n", mimetype(hd->reqpath));
    header = sdscatprintf(header, "Connection: %s\r\n", hd->keepalive ? "Keep-alive" : "Close");
    header = sdscat(header, "\r\n");
    hd->header = header;
    hd->headerLen = sdslen(header);
    hd->bodyLen = st.st_size;
    }

do_response:
    aeCreateFileEvent(server.evtLoop, fd, AE_WRITABLE, sendResponse, hd);
}

// whether find consecutive two empty line?
// TODO: get "Content-Length", and consume as much body.
static int requestCompleted(HttpData *hd)
{
    char *eoh = strstr(hd->buf, "\r\n\r\n");
    if (eoh) {
        sdstrunc(hd->buf, eoh - hd->buf + 2);
        return 1;
    }
    return 0;
}

static void readClientRequest(aeEventLoop *el, int fd, void *privdata, int mask)
{
    char buf[4096];
    int n;
    HttpData *hd = privdata;

    n = read(fd, buf, sizeof(buf));
    if (n == -1) {
        if (errno == EAGAIN)
            return;
        else
            goto closefd;
    } else if (n == 0) {
        if (server.verbosity)
            fprintf(stderr, "Client close connection\n");
        goto closefd;
    } else {
        if (sdslen(hd->buf) + n > MAX_REQUEST_HEAD_SIZE) {
            sendErrorResponse(hd, fd, "400 Invalid Request", "Request data exceeds");
            return;
        }
        hd->buf = sdscatlen(hd->buf, buf, n);
        if (requestCompleted(hd)) {
            processRequest(hd, fd);
        }
    }
    return;

closefd:
    aeDeleteFileEvent(el, fd, AE_READABLE);
    HttpData_free(hd);
    close(fd);
}

static void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask)
{
    int cport, cfd;
    char cip[128];
    HttpData *hd;

    cfd = anetTcpAccept(anetError, fd, cip, &cport);
    if (cfd == AE_ERR) {
        fprintf(stderr, "Accepting client connection: %s\n", anetError);
        return;
    }

    if (server.verbosity) {
        fprintf(stderr, "Accepted client connection from %s:%d\n", cip, cport);
    }

    hd = HttpData_create();
    if (hd == NULL) {
        oom("Creating HttpData");
    }
    anetNonBlock(anetError, cfd);
    aeCreateFileEvent(server.evtLoop, cfd, AE_READABLE, readClientRequest, hd);
}

int main(int argc, char **argv)
{
    int i, listenfd;

    progName = argv[0];
    memset(&server, 0, sizeof(server));
    server.port = 8080;     /* default port */
    server.docroot = sdsnew(".");

    for (i = 1; i < argc; i++) {
        int lastarg = (i == argc-1);
        if (strcmp(argv[i], "-h") == 0 && !lastarg) {
            server.host = argv[++i];
        } else if (strcmp(argv[i], "-p") == 0 && !lastarg) {
            server.port = atoi(argv[++i]);
        } else if (strcmp(argv[i], "-v") == 0 && !lastarg) {
            server.verbosity = atoi(argv[++i]);
        } else if (strcmp(argv[i], "-r") == 0 && !lastarg) {
            server.docroot = sdscpy(server.docroot, argv[++i]);
            /* TODO: check doc root */
        } else {
usage:
            printf("%s: simple http server\n", progName);
            printf("    -h <hostname>        Server hostname\n");
            printf("    -p <port>            Server port\n");
            printf("    -v <verbosity>       Verbose output\n");
            printf("    -r <docroot>         doc root\n");
            printf("    --help               Print this usage\n");
            exit(1);
        }
    }

    listenfd = anetTcpServer(anetError, server.port, server.host);
    if (listenfd == ANET_ERR) {
        fprintf(stderr, "Opening port: %s\n", anetError);
        exit(1);
    }

    server.evtLoop = aeCreateEventLoop();
    if (aeCreateFileEvent(server.evtLoop, listenfd, AE_READABLE, acceptTcpHandler, NULL) == AE_ERR) {
        oom("Creating file event");
    }
    aeMain(server.evtLoop);
    aeDeleteEventLoop(server.evtLoop);

    return 0;
}
